/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Globalization;
using System.Collections.Generic;
using System.Reflection;
using Xceed.Utils.Licensing;
using System.ComponentModel;

namespace Xceed.Silverlight.Controls
{
#if !WINDOWS_PHONE

  public class NumericTextBox : AutoSelectTextBox, INotifyPropertyChanged
  {
    #region STATIC MEMBERS

    private static NumberStyles DefaultNumberStyles = NumberStyles.Number;

    #endregion STATIC MEMBERS

    #region STATIC METHODS
    internal static bool IsNumericType( Type type )
    {
      if( type == null )
        return false;

      if( type.IsValueType )
      {
        if( ( type == typeof( int ) ) || ( type == typeof( double ) ) || ( type == typeof( decimal ) )
          || ( type == typeof( float ) ) || ( type == typeof( short ) ) || ( type == typeof( long ) )
          || ( type == typeof( ushort ) ) || ( type == typeof( uint ) ) || ( type == typeof( ulong ) )
          || ( type == typeof( byte ) ) || ( type == typeof( Nullable<int> ) ) || ( type == typeof( Nullable<double> ) )
          || ( type == typeof( Nullable<decimal> ) ) || ( type == typeof( Nullable<float> ) )
          || ( type == typeof( Nullable<short> ) ) || ( type == typeof( Nullable<long> ) )
          || ( type == typeof( Nullable<ushort> ) ) || ( type == typeof( Nullable<uint> ) )
          || ( type == typeof( Nullable<ulong> ) ) || ( type == typeof( Nullable<byte> ) )
          )
        {
          return true;
        }
      }

      return false;
    }

    #endregion

    #region CONSTRUCTORS

    public NumericTextBox()
    {
      this.AllowDrop = false;
      this.TextChanged += new TextChangedEventHandler( OnTextChanged );
    }

    #endregion CONSTRUCTORS

    #region DifferTextChange PROPERTY
    private bool DifferTextChanged
    {
      get;
      set;
    }
    #endregion

    #region OnTextChange EventHandler

    void OnTextChanged( object sender, TextChangedEventArgs e )
    {
      if( this.DifferTextChanged )
        return;

      int selectionStart = this.SelectionStart;

      try
      {

        this.DifferTextChanged = true;

        string compositionText = this.Text;

        if( String.IsNullOrEmpty( compositionText ) || String.IsNullOrWhiteSpace( compositionText ) )
        {
          if( this.Value != null )
            this.Value = null;
          return;
        }

        string resultingText = this.Text;

        if( String.IsNullOrEmpty( resultingText ) )
        {
          resultingText = string.Empty;
        }

        object notUsed;
        bool parsingSuccessful = this.TryParse( resultingText, out notUsed );

        if( !parsingSuccessful )
        {
          if( this.LastValidText != null )
          {
            this.Text = this.LastValidText;
          }
          else
          {
            this.Text = string.Empty;
          }
          this.SelectionStart = selectionStart;
        }
        this.LastValidText = this.Text;
        this.RefreshValue();
      }
      finally
      {
        this.DifferTextChanged = false;
      }

    }
    #endregion

    #region CustomDecimalKeys

    public List<Key> CustomDecimalKeys
    {
      get
      {
        if( m_customDecimalKeys == null )
          m_customDecimalKeys = new List<Key>( 1 );

        return m_customDecimalKeys;
      }
      set
      {
        if( value == null )
          throw new ArgumentNullException( "CustomDecimalKeys" );

        m_customDecimalKeys = value;
      }
    }

    #endregion CustomDecimalKeys

    #region NumberStyles PROPERTY

    public NumberStyles NumberStyles
    {
      get
      {
        return ( NumberStyles )GetValue( NumberStylesProperty );
      }
      set
      {
        SetValue( NumberStylesProperty, value );
      }
    }

    public static readonly DependencyProperty NumberStylesProperty = DependencyProperty.Register(
      "NumberStyles",
      typeof( NumberStyles ),
      typeof( NumericTextBox ),
      new PropertyMetadata( NumericTextBox.DefaultNumberStyles,
                            new PropertyChangedCallback( NumericTextBox.NumberStylesPropertyChanged ) ) );

    private static void NumberStylesPropertyChanged( DependencyObject sender, DependencyPropertyChangedEventArgs value )
    {
      NumericTextBox numericTextBox = sender as NumericTextBox;

      numericTextBox.ValidateNumberStyles( ( NumberStyles )value.NewValue );

    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly" )]
    private void ValidateNumberStyles( NumberStyles numberStyles )
    {
      if( Xceed.Utils.Licensing.DesignerProperties.GetIsInDesignMode() )
        return;

      string text = this.Text;

      object notUsed;
      bool parseSuccessful = this.TryParse( text, numberStyles, this.GetActiveFormatProvider(), out notUsed );

      if( !parseSuccessful )
        throw new InvalidOperationException( "The current text can not be parsed using the specified NumberStyles." );
    }

    #endregion NumberStyles PROPERTY

    #region LastValidText PROPERTY
    private string LastValidText
    {
      get;
      set;
    }
    #endregion

    #region FormatProvider Property

    public IFormatProvider FormatProvider
    {
      get
      {
        return ( IFormatProvider )GetValue( FormatProviderProperty );
      }
      set
      {
        SetValue( FormatProviderProperty, value );
      }
    }

    public static readonly DependencyProperty FormatProviderProperty =
        DependencyProperty.Register( "FormatProvider", typeof( IFormatProvider ), typeof( NumericTextBox ),
      new PropertyMetadata( null,
      new PropertyChangedCallback( NumericTextBox.FormatProviderPropertyChangedCallback ) ) );

    private static void FormatProviderPropertyChangedCallback( DependencyObject sender, DependencyPropertyChangedEventArgs e )
    {
      NumericTextBox numericTextBox = ( NumericTextBox )sender;

      if( !numericTextBox.IsInitialized )
        return;

      numericTextBox.OnFormatProviderChanged();
    }

    internal void OnFormatProviderChanged()
    {
      this.RefreshConversionHelpers();
      this.RefreshCurrentText( false );
      this.RefreshValue();
    }

    #endregion FormatProvider Property

    #region DataType Property

    public static readonly DependencyProperty DataTypeProperty = DependencyProperty.Register(
      "DataType",
      typeof( Type ),
      typeof( NumericTextBox ),
      new PropertyMetadata(
        ( Type )typeof( double ),
        new PropertyChangedCallback( NumericTextBox.OnDataTypeChanged ) ) );

    public Type DataType
    {
      get
      {
        return ( Type )this.GetValue( NumericTextBox.DataTypeProperty );
      }
      set
      {
        this.SetValue( NumericTextBox.DataTypeProperty, value );
      }
    }

    private static void OnDataTypeChanged( DependencyObject sender, DependencyPropertyChangedEventArgs e )
    {
      NumericTextBox textBox = sender as NumericTextBox;

      if( textBox == null )
        return;

      // Make sure initialization procedure is completed here
      if( !textBox.IsFinalizingInitialization )
      {
        textBox.IsNumericValueDataType = NumericTextBox.IsNumericType( textBox.DataType );
        textBox.RefreshConversionHelpers();
        textBox.RefreshValue();
      }

    }

    #endregion

    #region Value PROPERTY
    public object Value
    {
      get
      {
        return ( object )GetValue( ValueProperty );
      }
      set
      {
        SetValue( ValueProperty, value );
      }
    }

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register( "Value", typeof( object ), typeof( NumericTextBox ),
      new PropertyMetadata(
      new PropertyChangedCallback( NumericTextBox.ValuePropertyChangedCallback ) ) );


    private static void ValuePropertyChangedCallback( object sender, DependencyPropertyChangedEventArgs e )
    {
      NumericTextBox numericTextBox = sender as NumericTextBox;

      if( numericTextBox.IsForcingValue )
        return;

      // The ValueChangedCallback can be raised even though both values are the same since the property
      // datatype is Object.
      if( object.Equals( e.NewValue, e.OldValue ) )
        return;

      numericTextBox.IsInValueChanged = true;
      try
      {
        numericTextBox.Text = numericTextBox.GetTextFromValue( e.NewValue );
      }
      finally
      {
        numericTextBox.IsInValueChanged = false;
      }
    }

    #endregion Value Property

    #region HasValidationError Property

    public bool HasValidationError
    {
      get
      {
        return m_hasValidationError;
      }
      set
      {
        if( m_hasValidationError != value )
        {
          m_hasValidationError = value;
          this.RaisePropertyChanged( "HasValidationError" );
        }
      }
    }

    #endregion HasValidationError Property

    #region HasParsingError Property

    public bool HasParsingError
    {
      get
      {
        return m_hasParsingError;
      }
      set
      {
        if( m_hasParsingError != value )
        {
          m_hasParsingError = value;
          this.RaisePropertyChanged( "HasParsingError" );
        }
      }
    }

    #endregion HasParsingError Property

    #region TEXT FROM VALUE

    public event EventHandler<QueryTextFromValueEventArgs> QueryTextFromValue;

    internal string GetTextFromValue( object value )
    {
      string text = this.QueryTextFromValueCore( value );

      QueryTextFromValueEventArgs e = new QueryTextFromValueEventArgs( value, text );

      this.OnQueryTextFromValue( e );

      return e.Text;
    }

    private void OnQueryTextFromValue( QueryTextFromValueEventArgs e )
    {
      if( this.QueryTextFromValue != null )
        this.QueryTextFromValue( this, e );
    }

    #endregion TEXT FROM VALUE

    #region VALUE FROM TEXT

    public event EventHandler<QueryValueFromTextEventArgs> QueryValueFromText;

    internal object GetValueFromText( string text, out bool hasParsingError )
    {
      object value = null;
      bool success = this.QueryValueFromTextCore( text, out value );

      QueryValueFromTextEventArgs e = new QueryValueFromTextEventArgs( text, value );
      e.HasParsingError = !success;

      this.OnQueryValueFromText( e );

      hasParsingError = e.HasParsingError;

      return e.Value;
    }

    private void OnQueryValueFromText( QueryValueFromTextEventArgs e )
    {
      if( this.QueryValueFromText != null )
        this.QueryValueFromText( this, e );
    }

    #endregion VALUE FROM TEXT

    #region PROTECTED METHODS

    protected override void EndInitCore()
    {
      this.IsFinalizingInitialization = true;
      try
      {
        this.IsNumericValueDataType = NumericTextBox.IsNumericType( this.DataType );
        this.RefreshConversionHelpers();
      }
      catch( Exception exception )
      {
        throw new InvalidOperationException( "Initialization of the NumericTextBox failed.", exception );
      }
      finally
      {
        this.IsFinalizingInitialization = false;
      }
    }

    protected override void OnKeyDown( KeyEventArgs e )
    {
      base.OnKeyDown( e );

      if( !e.Handled )
      {
        if( e.Key == Key.Decimal )
        {
          string resultingText = this.Text;

          int oldSelectionStart = this.SelectionStart;

          if( String.IsNullOrEmpty( resultingText ) )
          {
            resultingText = string.Empty;
          }
          else
          {
            resultingText = resultingText.Remove( oldSelectionStart, this.SelectionLength );
          }

          CultureInfo currentCultureInfo = this.GetCultureInfo();

          resultingText = resultingText.Insert( this.SelectionStart, currentCultureInfo.NumberFormat.NumberDecimalSeparator );

          this.Text = resultingText;

          this.SelectionStart = oldSelectionStart + currentCultureInfo.NumberFormat.NumberDecimalSeparator.Length;

          e.Handled = true;
        }
      }
    }

    protected override void OnTextInput( System.Windows.Input.TextCompositionEventArgs e )
    {
      string compositionText = e.Text;

      if( String.IsNullOrEmpty( compositionText ) )
      {
        e.Handled = true;
        return;
      }

      string resultingText = this.Text;
      this.LastValidText = this.Text;

      if( String.IsNullOrEmpty( resultingText ) )
      {
        resultingText = string.Empty;
      }
      else
      {
        resultingText = resultingText.Remove( this.SelectionStart, this.SelectionLength );
      }

      resultingText = resultingText.Insert( this.SelectionStart, compositionText );

      object notUsed;
      bool parsingSuccessful = this.TryParse( resultingText, out notUsed );

      if( !parsingSuccessful )
      {
        e.Handled = true;
        return;
      }

      this.LastValidText = resultingText;
      base.OnTextInput( e );
    }

    protected override void OnGotFocus( RoutedEventArgs e )
    {
      base.OnGotFocus( e );

      this.RefreshCurrentText( true );
    }

    protected override void OnLostFocus( RoutedEventArgs e )
    {
      base.OnLostFocus( e );

      this.RefreshCurrentText( true );
    }

    protected bool QueryValueFromTextCore( string text, out object value )
    {
      Type type = this.DataType;

      if( type == null )
        throw new InvalidOperationException( "An attempt was made to query the value from the specified text when the DataType has not been specified." );

      return this.TryParse( text, out value );
    }

    protected string QueryTextFromValueCore( object value )
    {
      if( value != null )
      {
        if( ( this.NumberStyles & NumberStyles.HexNumber ) == NumberStyles.HexNumber )
        {
          return String.Format( this.GetActiveFormatProvider(), "{0:X}", value );
        }

        //This is required to make sure that default representation of value is not "against" the NumberStyles
        if( ( this.NumberStyles & NumberStyles.Number ) == NumberStyles.Number )
        {
          return String.Format( this.GetActiveFormatProvider(), "{0:N}", value );
        }
      }


      if( ( value == null ) || ( value == DBNull.Value ) )
        return string.Empty;

      IFormatProvider formatProvider = this.GetActiveFormatProvider();

      CultureInfo cultureInfo = formatProvider as CultureInfo;

      if( cultureInfo != null )
      {
        Type valueType = value.GetType();
        IConvertible convertibleInterface = valueType.GetInterface( "IConvertible", false ) as IConvertible;

        if( convertibleInterface != null )
          return ( string )convertibleInterface.ToString( this.FormatProvider );
      }

      try
      {
        string result = System.Convert.ToString( value, formatProvider );

        return result;
      }
      catch
      {
      }

      return value.ToString();

    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly" )]
    protected virtual void ValidateValue( object value )
    {
      if( value == null )
        return;

      Type validatingType = this.DataType;

      if( ( validatingType.IsGenericType ) && ( validatingType.GetGenericTypeDefinition() == typeof( Nullable<> ) ) )
        validatingType = Nullable.GetUnderlyingType( validatingType );

      if( validatingType == null )
        throw new InvalidOperationException( "An attempt was made to set a value when the DataType property is null." );

      if( ( value != DBNull.Value ) && ( value.GetType() != validatingType ) )
        throw new ArgumentException( "The value is not of type " + validatingType.Name + ".", "Value" );
    }

    #endregion PROTECTED METHODS

    #region INTERNAL PROPERTIES
    internal bool IsForcingValue
    {
      get;
      private set;
    }

    internal bool IsForcingText
    {
      get;
      private set;
    }

    internal bool IsNumericValueDataType
    {
      get;
      private set;
    }

    internal bool IsTextReadyToBeParsed
    {
      get
      {
        return true;
      }
    }

    #endregion

    #region INTERNAL METHODS

    internal void RefreshConversionHelpers()
    {
      Type type = this.DataType;

      if( type == null )
      {
        m_tryParseMethodInfo = null;
      }
      else
      {
        if( ( type.IsGenericType ) && ( type.GetGenericTypeDefinition() == typeof( Nullable<> ) ) )
          type = Nullable.GetUnderlyingType( type );

        m_tryParseMethodInfo = type.GetMethod(
          "TryParse",
          new Type[] { typeof( string ), typeof( NumberStyles ), typeof( IFormatProvider ), type.MakeByRefType() } );
      }

    }

    internal IFormatProvider GetActiveFormatProvider()
    {
      IFormatProvider formatProvider = this.FormatProvider;

      if( formatProvider != null )
        return formatProvider;

      return CultureInfo.CurrentCulture;
    }

    internal CultureInfo GetCultureInfo()
    {
      CultureInfo cultureInfo = this.GetActiveFormatProvider() as CultureInfo;

      if( cultureInfo != null )
        return cultureInfo;

      return CultureInfo.CurrentCulture;
    }

    internal string GetCurrentText()
    {
      return this.Text;
    }

    internal string GetParsableText()
    {
      return this.Text;
    }

    internal void RefreshCurrentText( bool preserveCurrentCaretPosition )
    {
      string displayText = this.GetCurrentText();

      if( !string.Equals( displayText, this.Text ) )
        this.ForceText( displayText, preserveCurrentCaretPosition );
    }

    internal void ForceValue( object value )
    {
      this.IsForcingValue = true;
      try
      {
        this.Value = value;
      }
      finally
      {
        this.IsForcingValue = false;
      }
    }

    internal void RefreshValue()
    {
      if( this.IsForcingValue )
        return;

      object value;
      bool hasParsingError;

      if( this.IsTextReadyToBeParsed )
      {
        string parsableText = this.GetParsableText();

        value = this.GetValueFromText( parsableText, out hasParsingError );

      }
      else
      {
        // We don't consider empty text as a parsing error.
        hasParsingError = !this.GetIsEditTextEmpty();
        value = null;
      }

      this.HasParsingError = hasParsingError;

      bool hasValidationError = hasParsingError;
      try
      {
        this.ValidateValue( value );
      }
      catch( Exception )
      {
        hasValidationError = true;
        value = null;
      }

      if( !object.Equals( value, this.Value ) )
        this.ForceValue( value );

      this.HasValidationError = hasValidationError;
    }

    internal bool GetIsEditTextEmpty()
    {
      return this.Text == string.Empty;
    }

    internal void ForceText( string text, bool preserveCaret )
    {
      this.IsForcingText = true;
      try
      {
        int oldSelectionStart = this.SelectionStart;

        this.Text = text;

        if( preserveCaret )
        {
          try
          {
            this.SelectionStart = oldSelectionStart;
          }
          catch( NullReferenceException )
          {
          }
        }
      }
      finally
      {
        this.IsForcingText = false;
      }
    }


    #endregion INTERNAL METHODS

    #region PRIVATE METHODS
    private bool TryParse( string text, out object parsedResult )
    {
      return this.TryParse( text, this.NumberStyles, this.GetActiveFormatProvider(), out parsedResult );
    }

    private bool TryParse( string text, NumberStyles numberStyles, IFormatProvider formatProvider, out object parsedResult )
    {
      parsedResult = null;

      if( text == string.Empty )
      {
        return true;
      }

      NumberFormatInfo numberFormatInfo = NumberFormatInfo.GetInstance( formatProvider );

      bool allowExponent = ( ( numberStyles & NumberStyles.AllowExponent ) == NumberStyles.AllowExponent );
      bool allowDecimalPoint = ( ( numberStyles & NumberStyles.AllowDecimalPoint ) == NumberStyles.AllowDecimalPoint );

      bool allowLeadingWhite = ( ( numberStyles & NumberStyles.AllowLeadingWhite ) == NumberStyles.AllowLeadingWhite );
      bool allowLeadingSign = ( ( numberStyles & NumberStyles.AllowLeadingSign ) == NumberStyles.AllowLeadingSign );

      string negativeSign = numberFormatInfo.NegativeSign;
      string positiveSign = numberFormatInfo.PositiveSign;

      string decimalSeparator = ( ( numberStyles & NumberStyles.Currency ) == NumberStyles.Currency ) ?
        numberFormatInfo.CurrencyDecimalSeparator : numberFormatInfo.NumberDecimalSeparator;

      if( ( allowExponent )
        && ( ( text.EndsWith( "e" ) ) || ( text.EndsWith( "E" ) )
        || ( text.EndsWith( "e" + negativeSign ) ) || ( text.EndsWith( "E" + negativeSign ) )
        || ( text.EndsWith( "e" + positiveSign ) ) || ( text.EndsWith( "E" + positiveSign ) ) ) )
      {
        text = text.Insert( text.Length, "0" );
      }

      if( allowLeadingWhite )
        text = text.TrimStart( new char[] { ' ' } );

      if( allowLeadingSign )
      {
        if( ( text.StartsWith( negativeSign ) && ( text.Length == negativeSign.Length ) )
          || ( text.StartsWith( positiveSign ) && ( text.Length == positiveSign.Length ) ) )
        {
          text = text.Insert( text.Length, "0" );
        }
      }

      if( ( allowDecimalPoint ) && ( text.EndsWith( decimalSeparator ) ) )
        text = text.Insert( text.Length, "0" );

      if( m_tryParseMethodInfo != null )
      {
        object[] tryParseParameters = new object[] { text, numberStyles, numberFormatInfo, null };

        if( ( bool )m_tryParseMethodInfo.Invoke( null, tryParseParameters ) )
        {
          parsedResult = ( object )tryParseParameters[ 3 ];
          return true;
        }
      }

      return false;
    }

    #endregion PRIVATE METHODS

    #region PRIVATE PROPERTIES
    internal bool IsInValueChanged
    {
      get;
      private set;
    }

    private bool IsFinalizingInitialization
    {
      get;
      set;
    }

    #endregion

    #region PRIVATE FIELDS

    private MethodInfo m_tryParseMethodInfo;
    private List<Key> m_customDecimalKeys;
    private bool m_hasValidationError;
    private bool m_hasParsingError;


    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged( string propertyName )
    {
      if( this.PropertyChanged != null )
      {
        this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
      }
    }


    #endregion

  }

#endif
}
